package com.lry;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@Slf4j
@Data
public class MappedFileQueue {

    private final String storePath;

    private final int mappedFileSize;

    private CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<>();

    private long flushedWhere;

    private long committedWhere;

    private volatile long storeTimestamp;

    private MappedFileCallback mappedFileCallback;
    public MappedFileQueue(final String storePath,final int mappedFileSize,MappedFileCallback mappedFileCallback){
        this.storePath = storePath;
        this.mappedFileSize = mappedFileSize;
        this.mappedFileCallback = mappedFileCallback;
    }
    public MappedFileQueue(final String storePath,final int mappedFileSize){
        this.storePath = storePath;
        this.mappedFileSize = mappedFileSize;
    }
    public MappedFile getLastMappedFile(final long startOffset){
        return getLastMappedFile(startOffset,true);
    }
    public MappedFile getLastMappedFile(final long startOffset,boolean needCreate){
        long createOffset = -1;
        MappedFile lastMappedFile = getLastMappedFile();

        if(null==lastMappedFile){
            createOffset = startOffset - (startOffset % this.mappedFileSize);
        }else{
            if(lastMappedFile.isFull()){
                createOffset = lastMappedFile.getFileFromOffset() + this.mappedFileSize;
            }
        }

        if(createOffset!=-1 && needCreate){
            return createMappedFile(createOffset);
        }

        return lastMappedFile;
    }


    private MappedFile createMappedFile(final long createOffset){
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
        MappedFile mappedFile = null;
        try {
            mappedFile = new MappedFile(nextFilePath,this.mappedFileSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if(null!=mappedFile){
            if(this.mappedFiles.isEmpty()){
                mappedFile.setFirstCreateInQueue(true);
            }
            this.mappedFiles.add(mappedFile);
        }
        return mappedFile;
    }


    public MappedFile getFirstMappedFile(){
        if(this.mappedFiles.isEmpty()){
            return null;
        }
        return this.mappedFiles.get(0);
    }

    public MappedFile getLastMappedFile(){
        if(this.mappedFiles.isEmpty()){
            return null;
        }
        return this.mappedFiles.get(mappedFiles.size()-1);
    }

    public boolean flush(){
        return flush(0);
    }

    public boolean flush(final int flushLeastPages){
        MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);

        if(null!=mappedFile){
            int flushPos = mappedFile.flush(flushLeastPages);
            this.flushedWhere = mappedFile.getFileFromOffset() + flushPos;
        }
        return true;
    }

    public boolean commit(){
        return commit(0);
    }

    public boolean commit(final int commitLeastPages){
        MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);

        if(null!=mappedFile){
            int commitPos = mappedFile.commit(commitLeastPages);
            this.committedWhere = mappedFile.getFileFromOffset() + commitPos;
        }
        return true;
    }

    public MappedFile findMappedFileByOffset(long offset) {
        return findMappedFileByOffset(offset,true);
    }

//    public MappedFile findMappedFileByOffset(long offset, boolean returnFirstOnNotFound) {
//        MappedFile firstMappedFile = getFirstMappedFile();
//        if(null==firstMappedFile){
//            return null;
//        }
//        int index = (int) (offset / mappedFileSize - firstMappedFile.getFileFromOffset()/mappedFileSize);
//
//        if(index<0 || index >= mappedFiles.size()){
//            if(returnFirstOnNotFound){
//                return firstMappedFile;
//            }
//            return null;
//        }
//
//        return mappedFiles.get(index);
//    }

    public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
        try {
            MappedFile firstMappedFile = this.getFirstMappedFile();
            MappedFile lastMappedFile = this.getLastMappedFile();

            if (firstMappedFile != null && lastMappedFile != null) {
                if (offset < firstMappedFile.getFileFromOffset() || offset > lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
                    log.warn("Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}",
                            offset,
                            firstMappedFile.getFileFromOffset(),
                            lastMappedFile.getFileFromOffset() + this.mappedFileSize,
                            this.mappedFileSize,
                            this.mappedFiles.size());
                } else {
                    int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
                    MappedFile targetFile = null;
                    try {
                        targetFile = this.mappedFiles.get(index);
                    } catch (Exception ignored) {
                    }

                    if (targetFile != null && offset >= targetFile.getFileFromOffset()
                            && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
                        return targetFile;
                    }

                    for (MappedFile tmpMappedFile : this.mappedFiles) {
                        if (offset >= tmpMappedFile.getFileFromOffset()
                                && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
                            return tmpMappedFile;
                        }
                    }
                }

                if (returnFirstOnNotFound) {
                    return firstMappedFile;
                }
            }
        } catch (Exception e) {
            log.error("findMappedFileByOffset Exception", e);
        }

        return null;
    }


    public long getMaxOffset() {
        MappedFile mappedFile = getLastMappedFile();
        if (mappedFile != null) {
            return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
        }
        return 0;
    }

    public long getMaxWrotePosition() {
        MappedFile mappedFile = getLastMappedFile();
        if (mappedFile != null) {
            return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
        }
        return 0;
    }


    public boolean load() {
        File dir = new File(this.storePath);
        File[] ls = dir.listFiles();
        if (ls != null) {
            return doLoad(Arrays.asList(ls));
        }
        return true;
    }

    public boolean doLoad(List<File> files) {
        // ascending order
        files.sort(Comparator.comparing(File::getName));

        for(int i=0;i<files.size();i++){
            File file = files.get(i);
            if (file.length() != this.mappedFileSize) {
                log.warn(file + "\t" + file.length()
                        + " length not matched message store config value, please check it manually");
                return false;
            }

            try {
                MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);

                int writePosition = this.mappedFileSize;
                if(i==files.size()-1 && null!=mappedFileCallback){
                    writePosition = mappedFileCallback.calWritePosition(mappedFile);
                }
                mappedFile.setWrotePosition(writePosition);
                mappedFile.setFlushedPosition(writePosition);
                mappedFile.setCommittedPosition(writePosition);
                this.mappedFiles.add(mappedFile);
                log.info("load " + file.getPath() + " OK");
            } catch (IOException e) {
                log.error("load file " + file + " error", e);
                return false;
            }
        }
        return true;
    }


    public long remainHowManyDataToCommit() {
        return getMaxWrotePosition() - committedWhere;
    }

    public long remainHowManyDataToFlush() {
        return getMaxOffset() - flushedWhere;
    }

    interface MappedFileCallback{
        int calWritePosition(MappedFile mappedFile);
    }
}
